package cn.flying.cloud.oauth.server.configuration.authentication;

import javax.annotation.Resource;
import javax.sql.DataSource;

import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.session.HttpSessionEventPublisher;

import cn.flying.cloud.base.redis.service.RedisService;
import cn.flying.cloud.oauth.server.configuration.captcha.CaptchaAuthenticationProvider;
import cn.flying.cloud.oauth.server.configuration.captcha.CaptchaWebAuthenticationDetailsSource;
import cn.flying.cloud.oauth.server.configuration.details.CustomUserDetailService;
import cn.flying.cloud.oauth.server.configuration.filter.JwtPreAuthenticationFilter;
import cn.flying.cloud.oauth.server.configuration.handler.CustomAuthenticationFailureHandler;
import cn.flying.cloud.oauth.server.configuration.handler.CustomAuthenticationSuccessHandler;
import cn.flying.cloud.oauth.server.configuration.handler.CustomAuthorAccessDeniedHandler;
import cn.flying.cloud.oauth.server.configuration.handler.CustomAuthorizedEntryPointHandler;
import cn.flying.cloud.oauth.server.configuration.handler.CustomLogoutSuccessHandler;
import cn.flying.cloud.oauth.server.configuration.provider.SmsCodeAuthenticationProvider;
import cn.flying.cloud.oauth.server.configuration.session.SessionExpiredStrategy;
import cn.flying.cloud.oauth.server.configuration.session.SessionInvalidStrategy;

/**
 * security配置，主要是配置请求访问权限、定义认证管理器、密码加密配置
 *
 * @author: admin
 * @date: 2023年06月19日 15:00
 * @version: 1.0
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
@EnableWebSecurity(debug = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Resource
    private PasswordEncoder passwordEncoder;
    @Resource
    private CustomUserDetailService customUserDetailService;
    @Resource
    private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
    @Resource
    private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
    @Resource
    private CustomAuthorizedEntryPointHandler customauthorizedEntryPointHandler;
    @Resource
    private CustomAuthorAccessDeniedHandler customAuthorAccessDeniedHandler;
    @Resource
    private CustomLogoutSuccessHandler customLogoutSuccessHandler;
    @Resource
    private SessionExpiredStrategy sessionExpiredStrategy;
    @Resource
    private SessionInvalidStrategy sessionInvalidStrategy;
    @Resource
    private CaptchaWebAuthenticationDetailsSource captchaWebAuthenticationDetailsSource;
    @Resource
    private DataSource dataSource;
    @Resource
    private RedisService redisService;
    @Resource
    private SmsCodeAuthenticationProvider smsCodeAuthenticationProvider;

    /**
     * 访问请求权限配置
     *
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //添加token过滤验证
        http.addFilterBefore(jwtPreAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        //开启表单验证
        http.formLogin()
                .loginPage("/oauth/login/form")
                //登陆请求的url地址，自定义即可
                .loginProcessingUrl("/oauth/user/login")
                .successHandler(customAuthenticationSuccessHandler)
                .failureHandler(customAuthenticationFailureHandler)
                //指定authenticationDetailsSource，用于扩展图片验证码的验证
                .authenticationDetailsSource(captchaWebAuthenticationDetailsSource);

        //请求访问控制，设置需要拦截的请求
        http.authorizeRequests()
                //配置可以直接访问的页面
                .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
                .antMatchers(
                        "/oauth/user/login",
                        "/oauth/user/register",
                        "/oauth/user/reset"
                ).permitAll()
                // 其余所有请求都要通过认证鉴权
                .anyRequest()
                .authenticated()
                .and()
                .exceptionHandling()
                // 匿名无权限访问
                .authenticationEntryPoint(customauthorizedEntryPointHandler)
                //认证用户无权限访问
                .accessDeniedHandler(customAuthorAccessDeniedHandler);

        //禁用匿名登录
//        http.anonymous().disable();

        //开启 Remember-Me 功能
        http.rememberMe()
                //指定 Remember-Me 功能自动登录过程使用的 UserDetailsService 对象，默认使用 Spring 容器中的 UserDetailsService 对象
                .userDetailsService(customUserDetailService)
                //指定在登录时“记住我”的 HTTP 参数，默认为 remember-me
                .rememberMeParameter("remember")
                .rememberMeCookieName("remember-me-cookie")
                //“记住我”的 Token 中的标识字段，默认是一个随机的 UUID 值
//                .key("")
                //“记住我” 的 Token 令牌有效期，单位为秒，即对应的 cookie 的 Max-Age 值，默认时间为 2 周
                .tokenValiditySeconds(2 * 24 * 60 * 60)
                //指定 TokenRepository 对象，用来配置持久化 Token
                .tokenRepository(tokenRepository());

        //session管理配置，这里需要配置session
        //session管理配置
        http.sessionManagement()
                //是session的生成策略，前后端分离的时候配置成STATELESS，非前后端分离的需要配置成ALWAYS，这里用ALWAYS，即总是创建HttpSession，默认是IF_REQUIRED
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                //session失效后跳转路径（会话过期）
                //session失效策略，和invalidSessionUrl二选一
                .invalidSessionStrategy(sessionInvalidStrategy)
                //session保护  migrateSession默认（防御会话固定攻击）
                .sessionFixation().migrateSession()
                //限制最大登录用户（客户端）数 （会话并发控制）
                .maximumSessions(1)
                //true 表示已经登录了就不能在登录了，false表示允许再次登录但是会将原来登录的用户踢下线，使用这个需要配置一个Bean（HttpSessionEventPublisher）
                .maxSessionsPreventsLogin(false)
                // .expiredUrl("")
                //session到期策略
                .expiredSessionStrategy(sessionExpiredStrategy);

        //登出配置
        http.logout()
                .logoutUrl("/oauth/user/logout")
                .logoutSuccessHandler(customLogoutSuccessHandler)
                // 指定用户注销登录时删除的 Cookie
                .deleteCookies("JSESSIONID")
                // 用户注销登录时是否立即清除用户认证信息 Authentication，默认为 true
                .clearAuthentication(true);

        //开启跨域配置，禁用csrf防御机制,是禁用使用跨站请求,可解决POST请求403问题
        http.cors().and().csrf().disable().headers().frameOptions().disable();

        // 提供其他的认证方式（短信验证码认证）
        http.authenticationProvider(smsCodeAuthenticationProvider);
    }

    /**
     * 配置忽略资源
     * 一般是配置系统静态资源用，配置的请求根本不会进入 spring security 的过滤器链，直接放行
     * .antMatchers("/").permitAll() 是会进入 spring security 的过滤器链的，这是 2 者的主要区别
     * /login（登录）、/captcha（获取图片验证码）、/forget（忘记密码）、/register（注册）、/error（错误页面）
     *
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(
                "/api/oauth/static/**",
                "/oauth/static/**",
                "/static/**",
                "/css/**",
                "/font/**",
                "/images/**",
                "/js/**",
                "/layui/**",
                "/favicon.ico",
                //登录页面直接忽略，不进入过滤链
                "/oauth/login/form",
                "/oauth/register/form",
                "/oauth/forget/form",
//                "/oauth/register",
                "/oauth/user/captcha",
                "/oauth/user/send/code",
                // 获取公钥路由直接放开，不要进入过滤链，因为没有认证
                "/rsa/publicKey"
        );
    }

    /**
     * 定制用户认证管理器来实现用户认证，测试可以采用内存inMemoryAuthentication
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 指定UserDetailService和加密器，不再使用内存方式存储用户认证信息，而是动态从数据库中获取
        auth.userDetailsService(customUserDetailService).passwordEncoder(passwordEncoder);
    }

    /**
     * 认证管理器
     * 如果不配置SpringBoot会自动配置一个AuthenticationManager,覆盖掉内存中的用户
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * Jwt过滤器，拦截校验所有请求携带的Token
     *
     * @return
     */
    @Bean
    public JwtPreAuthenticationFilter jwtPreAuthenticationFilter() throws Exception {
        JwtPreAuthenticationFilter filter = new JwtPreAuthenticationFilter(authenticationManager());
        filter.setUnauthorizedEntryPointHandler(customauthorizedEntryPointHandler);
        filter.setUserDetailService(customUserDetailService);
        filter.setRedisService(redisService);
        return filter;
    }

//    /**
//     * 加密器
//     *
//     * @return
//     */
//    @Bean
//    public PasswordEncoder passwordEncoder {
//        return new BCryptPasswordEncoder();
////        return new PasswordEncoder() {
////            @Override
////            public String encode(CharSequence rawPassword) {
////                return CryptHelper.encryptWithSm4(rawPassword.toString());
////            }
////
////            @Override
////            public boolean matches(CharSequence rawPassword, String encodedPassword) {
////                return rawPassword.toString().equals(CryptHelper.decryptWithSm4(encodedPassword));
////            }
////        };
//    }

    /**
     * 初始化一个自定义的图片验证码校验provider
     *
     * @return
     */
    @Bean
    public CaptchaAuthenticationProvider captchaAuthenticationProvider() {
        CaptchaAuthenticationProvider provider = new CaptchaAuthenticationProvider();
        provider.setPasswordEncoder(passwordEncoder);
        provider.setUserDetailsService(customUserDetailService);
        //修改属性，当用户名查不到时可以明确抛出UsernameNotFoundException ，而不是抛出BadCredentialsException
        //具体可以查看AbstractUserDetailsAuthenticationProvider类的认证逻辑
        provider.setHideUserNotFoundExceptions(false);
        return provider;
    }

    /**
     * 配置 Session 的监听器（如果使用并发 Sessoion 控制，一般都需要配置）
     * 解决 Session 失效后, SessionRegistry 中 SessionInformation 没有同步失效问题
     * <p>
     * logout 退出后需要发布一个事件，通过将HttpSessionEventPublisher注入到ioc容器中，实现session的清除，
     * 如果配置http.maxSession.timeout，注入bean才会生效
     *
     * @return
     */
    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {

        return new HttpSessionEventPublisher();
    }

    /**
     * 注入SessionRegistry到ioc容器，便于业务处理使用，如需要获取session数量
     *
     * @return
     */
    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    /**
     * 配置 JdbcTokenRepositoryImpl，用于 Remember-Me 的持久化 Token
     */
    @Bean
    public JdbcTokenRepositoryImpl tokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        // 配置数据源
        jdbcTokenRepository.setDataSource(dataSource);
        // 第一次启动的时候可以使用以下语句自动建表（可以不用这句话，自己手动建表，源码中有语句的）
//        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

}
